iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
ChatGPT & Codex

我的音樂不該被平台綁架:30 天用 ChatGPT × Codex 打造跨平台 Music Recap系列 第 1

Day 1|把散落各平台的音樂紀錄,變成自己的 Music Recap

  • 分享至 

  • xImage
  •  

Day 1|把散落各平台的音樂紀錄,變成自己的 Music Recap

為什麼想做這個?

我平常主要使用 YouTube Music 聽歌,也滿常看 YouTube Music 提供的季度 Recap 和年度 Recap。

它會告訴我這段時間最常聽哪些歌曲、最常聽哪些歌手,以及一些個人化的音樂統計。

但我開始想到一個問題:

如果我的音樂紀錄散落在不同平台呢?

假設今天同時使用 YouTube Music、Spotify,甚至 Apple Music,那每個平台只會看到自己平台內的紀錄。

Spotify 不知道我在 YouTube Music 聽了什麼,YouTube Music 也不知道我在 Spotify 聽了什麼。

這樣算出來的年度回顧,其實都只是其中一部分。

所以這次參加 iThome 鐵人賽,我想試著做一件事:

使用 ChatGPT 與 Codex,打造一套可以整合不同音樂平台聆聽紀錄的 Music Recap。

除了常見的 Top Songs、Top Artists 和聆聽時間之外,我也希望最後可以做出一些官方 Recap 沒有提供的統計。

例如:

  • 哪一首歌曾經被我瘋狂循環播放?
  • 最長連續幾天聽同一位歌手?
  • 哪個月份突然開始大量聽某位歌手?
  • 半夜最常出現的歌手是誰?
  • 今年新發現了多少歌曲?
  • 哪些歌曲曾經突然爆聽,之後卻完全消失?
  • 不同月份的音樂偏好有什麼變化?

如果最後真的能把不同平台的紀錄放在一起,應該就可以做出一份真正屬於自己的 Music Recap。


第一個問題:資料拿得到嗎?

這其實是我最擔心的事情。

想法再漂亮,如果平台根本不讓使用者取得自己的播放紀錄,那這個專案做到最後可能什麼都沒有。

所以 Day 1 第一件事,就是先確認各平台到底能取得哪些資料。

目前我先研究三個平台:

  • YouTube Music
  • Spotify
  • Apple Music

Spotify

Spotify 提供個人資料匯出功能,其中 Extended Streaming History 可以取得相當完整的串流紀錄。

官方文件列出的資料包含:

  • 歌曲名稱
  • 歌手名稱
  • 專輯名稱
  • Spotify Track URI
  • 播放時間
  • 實際播放毫秒數
  • 串流結束時間

其中最重要的是「實際播放毫秒數」。

因為有這個欄位,就可以知道某次播放到底聽了多久,而不是只知道「曾經播放過這首歌」。

所以 Spotify 目前看起來會是這個專案最穩定的資料來源之一。


YouTube Music

Google 可以透過 Google Takeout 匯出 YouTube 與 YouTube Music 的相關資料。

問題是 YouTube Music 和一般 YouTube 本身的歷史紀錄存在一定程度的關聯。

所以目前還不能直接假設:

Google Takeout 匯出的每一筆 YouTube 歷史都是 YouTube Music 的歌曲播放紀錄。

實際拿到資料之後,還需要研究:

  • 哪些紀錄是 YouTube Music?
  • 哪些是一般 YouTube 影片?
  • 有沒有歌曲或影片 ID?
  • 時間欄位代表什麼?
  • 是否能知道實際播放多久?
  • Official Music Video 和純音訊版本要怎麼處理?

因為我自己主要使用 YouTube Music,所以接下來會優先研究這個平台。


Apple Music

Apple 也提供使用者申請個人資料副本,其中包含 Apple Music 的服務使用資訊。

不過目前還沒有實際取得匯出檔,所以我暫時不會假設 Apple Music 一定可以取得和 Spotify 一樣完整的資料。

因此第一版系統會先以:

YouTube Music + Spotify

作為主要目標。

Apple Music 則會先保留擴充的位置,等真正拿到資料之後再決定支援程度。

這樣就算 Apple Music 最後能取得的欄位比較少,也不會把整個專案卡住。


先建立統一的資料格式

不同平台最大的問題之一,就是資料格式一定不一樣。

Spotify 可能有:

trackName
artistName
albumName
msPlayed
spotify_track_uri

YouTube Music 可能提供的欄位又完全不同。

如果後面的統計程式每次都要知道資料來自哪個平台,整個系統很快就會變得很難維護。

所以我打算先把不同平台的資料轉成同一種格式。

暫時把一筆播放紀錄稱為:

ListeningEvent

例如:

{
  "event_id": "demo-005",
  "source": "youtube_music",
  "occurred_at": "2026-09-01T10:25:00+08:00",
  "timestamp_semantics": "source_event_time",
  "source_track_id": "demo-yt-night",
  "raw_title": "夜色",
  "raw_artist": "示範樂團",
  "played_ms": null,
  "duration_kind": "unknown"
}

目前最重要的欄位大概有:

event_id
source
occurred_at
source_track_id
raw_title
raw_artist
played_ms

平台原本提供的歌曲名稱和歌手名稱,也會先完整保留下來。

因為跨平台歌曲名稱的整理,會是之後另外一個大問題。


不知道的資料,就不要假裝知道

在設計資料格式的時候,很快就碰到第一個很容易算錯的地方。

假設我知道:

某首歌長度是 4 分鐘。

播放紀錄也顯示:

我曾經播放過這首歌。

那可以直接認定我聽了 4 分鐘嗎?

不行。

因為我可能:

  • 聽 10 秒就切歌
  • 聽到一半離開
  • 播放完整首歌
  • 不小心按到後立刻跳走

所以:

歌曲長度不能直接當成實際收聽時間。

如果來源沒有提供實際播放時長,我會把:

"played_ms": null

保留下來。

null 的意思是:

不知道。

而不是 0。

這兩個狀態必須分開。

例如:

"played_ms": 0

代表來源真的告訴我們這次播放時間是 0。

但:

"played_ms": null

則代表來源根本沒有這項資訊。

如果一開始沒有把這兩種情況分開,之後計算總聆聽時間就很容易產生錯誤。


「播放一筆」也不代表「完整聽完一首」

另外一個要先定義清楚的問題是:

如果歷史紀錄裡面有一筆:

YOASOBI - アイドル

我們只能確定:

系統記錄到一次相關播放事件。

不能直接說:

我完整聽完了一次《アイドル》。

所以未來在文章和系統裡,我會區分:

活動紀錄數

以及:

完整播放次數

除非資料真的能證明歌曲有完整播放,否則不會把兩者混在一起。


Day 1 先做一個最小實驗

今天先沒有急著處理真正的 Spotify 或 YouTube Music 匯出檔。

我先建立了一份假的測試資料。

例如:

[
  {
    "event_id": "demo-001",
    "source": "spotify",
    "raw_title": "晨光",
    "raw_artist": "示範歌手",
    "played_ms": 180000
  },
  {
    "event_id": "demo-002",
    "source": "youtube_music",
    "raw_title": "夜色",
    "raw_artist": "示範樂團",
    "played_ms": null
  }
]

接著先寫一個很小的 Python 程式,讓它可以:

  1. 讀取播放紀錄
  2. 檢查資料格式
  3. 處理重複紀錄
  4. 統計有時長與沒有時長的資料
  5. 計算目前確定知道的播放時間

這份測試資料總共有 8 筆紀錄。

結果如下:

項目 結果
活動紀錄數 8
有時長的紀錄 4
未知時長的紀錄 4
明確記錄為 0 秒 1
已知時長加總 8 分鐘
時長欄位覆蓋率 50%

這裡有一個很重要的地方。

「8 分鐘」只能代表:

目前資料中,可以確定知道的播放時間加起來是 8 分鐘。

不能說:

總共聽了 8 分鐘。

因為還有 4 筆紀錄根本不知道播放多久。

同樣的:

時長欄位覆蓋率 50%

也只是代表:

8 筆測試紀錄裡有 4 筆提供播放時間。

不是代表目前已經取得完整音樂歷史的一半。


重複資料怎麼辦?

之後匯入音樂紀錄,很可能遇到同一份資料被匯入兩次。

例如第一次匯入:

event-123

第二次又匯入:

event-123

如果內容完全相同,就不應該重複計算。

所以目前先使用:

source + event_id

作為其中一種重複判斷方式。

如果 ID 一樣,而且內容也一樣,就視為重複紀錄。

但如果 ID 一樣,內容卻不一樣,就不能偷偷選其中一筆。

系統應該直接回報:

Conflict

讓問題被發現。


還有一個更大的問題:同一首歌到底算不算同一首?

目前 Day 1 還沒有處理這件事。

但我已經可以預見,它可能會是整個專案最有趣的技術問題之一。

例如:

YOASOBI - アイドル

在另一個平台可能變成:

YOASOBI「アイドル」Official Music Video

甚至可能還有:

Idol
アイドル (Live)
アイドル - English Version
アイドル (Remastered)

哪些是同一首歌?

哪些應該分開統計?

單純把名稱轉成小寫再比對一定不夠。

所以目前的原則是:

先保留平台原始資料,不急著自動合併。

歌曲 Identity Matching 會在之後的文章另外處理。


ChatGPT 和 Codex 要怎麼分工?

既然這次參加的是 ChatGPT & Codex 主題競賽,我也希望這個系列不是單純叫 AI 幫我生程式碼。

目前預計的分工是:

ChatGPT

主要負責:

  • 研究平台資料來源
  • 分析問題
  • 討論系統設計
  • 設計資料模型
  • 找出統計可能出現的錯誤
  • 討論演算法
  • 分析結果
  • 協助整理文章

Codex

主要負責:

  • 在實際專案中閱讀程式
  • 實作功能
  • 修改程式
  • 執行測試
  • Debug
  • 重構程式
  • 檢查不同模組之間的整合

另外我也建立了一份 AGENTS.md

這個檔案會放一些 Codex 在這個專案裡必須遵守的規則。

例如:

未知時長不能自行補成歌曲完整長度。

不同平台歌曲名稱相似,不代表一定是同一首歌。

測試資料必須明確標示為 Demo Data。

不能為了讓統計結果完整,而自行猜測缺少的資料。

希望後面專案越來越大時,AI 不會做到一半突然忘記前面的規則。


有些很酷的功能,其實現在做不到

一開始我也想到:

能不能算出我是某位歌手全球前 0.01% 的聽眾?

YouTube Music 有時候會提供類似的徽章。

這看起來很適合放在自己的 Recap。

但是仔細想就會發現:

如果我只有自己的播放紀錄,我根本不知道其他人的分布。

沒有所有聽眾的資料,就無法真的知道自己位於:

Top 10%
Top 1%
Top 0.1%
Top 0.01%

所以這個功能目前先排除。

未來可以設計自己的:

忠誠度
單曲循環程度
探索率
回鍋率

但這些都會使用我們自己公開定義的演算法。

不會假裝它是官方的全球排名。


Day 1 做到哪裡?

今天沒有急著做漂亮的 Recap 頁面。

目前先完成的是整個專案最底層的基礎:

  • 確認 Spotify 可以取得完整串流歷史
  • 確認 YouTube / YouTube Music 有 Google Takeout 匯出管道
  • Apple Music 保留後續擴充
  • 決定第一版先支援 YouTube Music + Spotify
  • 建立統一的 ListeningEvent 概念
  • 區分未知播放時間和 0 秒播放
  • 不把活動紀錄直接當成完整播放
  • 建立最小 Python 統計程式
  • 建立 Demo Data
  • 建立基礎測試
  • 建立 Codex 使用的 AGENTS.md

目前使用的還全部都是測試資料。

真正的跨平台 Music Recap,還沒有開始。

但至少已經確定:

這個題目真的有資料可以做,而且最基本的架構是走得通的。

接下來最重要的,就是把假資料換成真正的音樂紀錄。


下一篇

Day 2|把我的 YouTube Music 紀錄挖出來:Google Takeout 到底給了什麼?

下一篇會先從我最常使用的 YouTube Music 開始。

我會實際從 Google Takeout 匯出資料,看看拿到的檔案到底長什麼樣子。

接著確認:

  • 有哪些欄位?
  • 能不能辨認歌曲?
  • YouTube 和 YouTube Music 怎麼區分?
  • 有沒有時間資訊?
  • 有沒有影片或歌曲 ID?
  • 實際播放時間拿不拿得到?
  • 有多少資料真的可以拿來做 Music Recap?

下一篇開始,就要正式碰真實資料了。


下一篇
Day 2|把我的 YouTube Music 紀錄挖出來:Google Takeout 到底給了什麼?
系列文
我的音樂不該被平台綁架:30 天用 ChatGPT × Codex 打造跨平台 Music Recap5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言